﻿using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Phoenix.Diagnostics
{
    public class TraceScope : IDisposable
    {
        readonly TraceLevel traceLevel;
        readonly Func<string> task;
        readonly Type sourceType;
        readonly Stopwatch stopwatch;
        bool hasChildTraces;
        static readonly Stack<TraceScope> disposeOrder = new Stack<TraceScope>();
        static readonly TraceScope dummyTraceScope = new TraceScope();
        bool disposed;

        public static TraceScope Create(object source, TraceLevel traceLevel, Func<string> task)
        {
            return Create(
                source == null ? typeof(TraceScope) : source.GetType(),
                traceLevel, 
                task);
        }

        public static TraceScope Create(Type sourceType, TraceLevel traceLevel, Func<string> task)
        {
            return Trace.WillTrace(traceLevel, sourceType) ? new TraceScope(sourceType, traceLevel, task) : dummyTraceScope;
        }

        TraceScope()
        { }

        TraceScope(Type sourceType, TraceLevel traceLevel, Func<string> task)
        {
            this.traceLevel = traceLevel;
            this.task = task;
            this.sourceType = sourceType;
            stopwatch = new Stopwatch();

            Trace.BeginTraceScope(write =>
                                      {
                                          write(traceLevel, sourceType, task());
                                          write(traceLevel, sourceType, "{");
                                          Trace.Indent();
                                          hasChildTraces = true;
                                      });
            disposeOrder.Push(this);
            stopwatch.Start();
        }

        void Write(Func<string> log)
        {
            switch (traceLevel)
            {
                case TraceLevel.Debug:
                    Trace.WriteDebug(sourceType, log);
                    break;
                case TraceLevel.Info:
                    Trace.WriteInfo(sourceType, log);
                    break;
                case TraceLevel.Warning:
                    Trace.WriteWarning(sourceType, log);
                    break;
                case TraceLevel.Error:
                    Trace.WriteError(sourceType, log);
                    break;
                default:
                    Trace.WriteError(this, ()=>"TraceScope doesn't recognise trace level " + traceLevel);
                    break;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposing) return;
            if (stopwatch == null || disposed) return;
            disposed = true;
            stopwatch.Stop();
            var toDisposeCheck = disposeOrder.Pop();
            if (!ReferenceEquals(toDisposeCheck, this))
                throw new InvalidOperationException(
                    string.Format("Trace Scope with text {0} disposed in the incorrect order, expected {1}", toDisposeCheck.task(), task()));

            if (hasChildTraces)
            {
                Trace.Unindent();
                Write(() => string.Format("}} {0}ms", stopwatch.ElapsedMilliseconds));
            }
            else
            {
                Trace.ClearTraceScopeCallback();
                Write(() => string.Format("{0}... {1}ms", task(), stopwatch.ElapsedMilliseconds));
            }
        }
    }
}